﻿//--------------------------------------------------------------------------------------------------------------------------------
// <copyright file="Grid1D.cs" company="Exul">
//     Copyright (c) Exul. All rights reserved.
// </copyright>
// <author>Exul</author>
//--------------------------------------------------------------------------------------------------------------------------------

namespace ExulLibrary.FiniteElementMethod
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;

    /// <summary>Represents one-dimensional grid.</summary>
    public sealed partial class Grid1D
    {
        /// <summary>The grid points.</summary>
        private readonly List<double> points = new List<double>();

        /// <summary>Gets the collection of grid points.</summary>
        /// <value>The collection of grid points.</value>
        public IList<double> Points
        {
            get
            {
                return this.points;
            }
        }

        /// <summary>Determines whether this grid is valid.</summary>
        /// <param name="errorMessage">The error message if the grid is invalid; otherwise, empty string.</param>
        /// <returns><see langword="true"/> if the specified grid is valid; otherwise, <see langword="false"/>.</returns>
        public bool IsValid(out string errorMessage)
        {
            var count = this.points.Count;
            for (var index = 0; index < count - 1; index++)
            {
                if (1e-16 <= this.points[index + 1] - this.points[index])
                {
                    continue;
                }

                errorMessage = string.Format(
                    CultureInfo.CurrentCulture,
                    Properties.Resources.GridElementHasNegativeSize,
                    index,
                    this.points[index],
                    this.points[index + 1],
                    1e-16);
                return false;
            }

            errorMessage = string.Empty;
            return true;
        }

        /// <summary>Adds sequence of points from start point to end point with the specified exhaustion.</summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="initialStep">The initial step.</param>
        /// <param name="exhaustion">The exhaustion.</param>
        /// <param name="skipFirst">If set to <see langword="true"/>, then start point is not added.</param>
        /// <param name="skipLast">If set to <see langword="true"/>, then end point is not added.</param>
        /// <exception cref="ArgumentException">
        ///     <paramref name="endPoint"/> is less than <paramref name="startPoint"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     <paramref name="initialStep"/> or <paramref name="exhaustion"/> is negative or equal to zero.
        /// </exception>
        public void AddSequenceFromStart(
            double startPoint,
            double endPoint,
            double initialStep,
            double exhaustion,
            bool skipFirst = false,
            bool skipLast = false)
        {
            #if SAFE

            if (endPoint < startPoint)
            {
                throw ThrowHelper.EndValueLessThanStartValue("startPoint", startPoint, "endPoint", endPoint);
            }

            if (initialStep <= 0.0)
            {
                throw ThrowHelper.StepIsNegativeOrEqualToZero("initialStep", initialStep);
            }

            if (exhaustion <= 0.0)
            {
                throw ThrowHelper.ExhaustionIsNegativeOrEqualToZero("exhaustion", exhaustion);
            }

            #endif

            if (!skipFirst)
            {
                this.points.Add(startPoint);
            }

            var step = initialStep;
            var value = startPoint;

            while ((value + step) <= endPoint)
            {
                value += step;
                this.points.Add(value);

                step *= exhaustion;
            }

            if (Math.Abs(endPoint - value) <= 1e-16)
            {
                if (skipLast)
                {
                    this.points.RemoveAt(this.points.Count - 1);
                }
            }
            else if (!skipLast)
            {
                this.points.Add(endPoint);
            }
        }

        /// <summary>Adds sequence of points from end points to start point with the specified exhaustion.</summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="initialStep">The initial step.</param>
        /// <param name="exhaustion">The exhaustion.</param>
        /// <param name="skipFirst">If set to <see langword="true"/>, then start point is not added.</param>
        /// <param name="skipLast">If set to <see langword="true"/>, then end point is not added.</param>
        /// <exception cref="ArgumentException">
        ///     <paramref name="endPoint"/> is less than <paramref name="startPoint"/>.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        ///     <paramref name="initialStep"/> or <paramref name="exhaustion"/> is negative or equal to zero.
        /// </exception>
        public void AddSequenceFromEnd(
            double startPoint,
            double endPoint,
            double initialStep,
            double exhaustion,
            bool skipFirst = false,
            bool skipLast = false)
        {
            #if SAFE

            if (endPoint < startPoint)
            {
                throw ThrowHelper.EndValueLessThanStartValue("startPoint", startPoint, "endPoint", endPoint);
            }

            if (initialStep <= 0.0)
            {
                throw ThrowHelper.StepIsNegativeOrEqualToZero("initialStep", initialStep);
            }

            if (exhaustion <= 0.0)
            {
                throw ThrowHelper.ExhaustionIsNegativeOrEqualToZero("exhaustion", exhaustion);
            }

            #endif

            if (!skipLast)
            {
                this.points.Add(endPoint);
            }

            var step = initialStep;
            var value = startPoint;

            var insertIndex = 1;

            while (startPoint <= (value - step))
            {
                value -= step;
                this.points.Insert(this.points.Count - insertIndex, value);

                step *= exhaustion;
                insertIndex++;
            }

            if (Math.Abs(startPoint - value) <= 1e-16)
            {
                if (skipFirst)
                {
                    this.points.RemoveAt(this.points.Count - insertIndex);
                }
            }
            else if (!skipFirst)
            {
                this.points.Insert(this.points.Count - insertIndex, startPoint);
            }
        }

        /// <summary>Converts the location in space to location in this grid.</summary>
        /// <param name="location">The location in space.</param>
        /// <param name="allowEmptyLocation">
        ///     If <see langword="true"/>, then left and right borders in the grid can be equal.
        /// </param>
        /// <returns>The location in grid.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="location"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="location"/> is out of this grid.</exception>
        public LocationInGrid GetLocationInGrid(Location1D location, bool allowEmptyLocation = false)
        {
            #if SAFE

            if (location == null)
            {
                throw Mathematics.LinearAlgebra.ThrowHelper.ArgumentIsNull("location");
            }

            if (location.Left < this.points[0])
            {
                throw ThrowHelper.BorderIsOutOfGrid("location", location.Left, this.points[0], "Left");
            }

            if (this.points[this.points.Count - 1] < location.Right)
            {
                throw ThrowHelper.BorderIsOutOfGrid(
                    "location", location.Right, this.points[this.points.Count - 1], "Right");
            }

            #endif

            var index = 0;
            var pointsCount = this.points.Count;
            while ((index < pointsCount - 1) && (location.Left < this.points[index]))
            {
                index++;
            }

            var leftIndex = Math.Abs(location.Left - this.points[index]) < Math.Abs(this.points[index + 1] - location.Left)
                                ? index
                                : index + 1;
            index = leftIndex;

            while ((index < pointsCount - 1) && (location.Right < this.points[index]))
            {
                index++;
            }

            var rightIndex = Math.Abs(location.Right - this.points[index]) < Math.Abs(this.points[index + 1] - location.Right)
                                 ? index
                                 : index + 1;
            if (!allowEmptyLocation && (leftIndex == rightIndex))
            {
                if (rightIndex != pointsCount - 1)
                {
                    rightIndex++;
                }
                else
                {
                    leftIndex--;
                }
            }

            return new LocationInGrid(leftIndex, rightIndex);
        }
    }
}